;定义内核地址在0x00040000
core_base_address equ 0x00040000
;定义内核所在的起始扇区在0x00000001
core_start_sector equ 0x00000001

;把代码段寄存器的值赋值给ax
mov ax,cs 
;把ax的值赋值给栈段寄存器ss
mov ss,ax
;把0x7c00赋值给栈指针寄存器sp
mov sp,0x7c00

;把cs:pgdt+0x7c00+0x02的值(0x00007e00)赋值给eax
mov eax,[cs:pgdt+0x7c00+0x02]
;将edx的值赋值为0
xor edx,edx
;给ebx赋值16
mov ebx,16
;将eax的值除以ebx（16）
div ebx
;此时eax的值是gdt的起始地址
;把eax的值赋值给数据段寄存器ds
mov ds,eax
;此时edx的值是get的偏移地址
;把edx的值赋值给ebx
mov ebx,edx
;不要0号GDT了，从1号GDT开始
;把0x0000ffff赋值给1号GDT的Limit
mov dword [ebx+0x08],0x0000ffff
;把0x00cf9200赋值给1号GDT的Base
;此时1号描述符的基地址是0，段界限是0xfffff
;粒度是4k
mov dword [ebx+0x0c],0x00cf9200

;构建2号描述符
;基地址是7c00，段界限是0x01ff，粒度为1字节
;用于访问主引导扇区所在的内存区域
mov dword [ebx+0x010],0x7c0001ff
mov dword [ebx+0x014],0x00409800

;构建三号描述符
;基地址是7c00,段界限是ffffe，粒度是4kb，实际大小是4GB
;向下扩展，是栈段
mov dword [ebx+0x018],0x7c00fffe
mov dword [ebx+0x01c],0x00cf9600

;构建4号描述符
;基地址是B8000，段界限是0x07fff，粒度是字节
;保护模式的显示缓冲区描述符
mov dword [ebx+0x20],0x80007fff
mov dword [ebx+0x24],0x0040920b

;初始化描述符表寄存器GDTR
mov word [cs:pgdt + 0x7c00],39

;加载GDTR
lgdt [cs: pgdt + 0x7c00]

;以下都是开启保护模式的代码
in al,0x92
or al,0000_0010B
out 0x92,al

cli

mov eax,cr0
or eax,1
mov cr0,eax
;保护模式开启完成
;原转移，跳到保护模式的代码
jmp 0x0010:flush

;使用32位的工作模式
[bits 32]
flush:
    ;将段选择子0x0008赋值给eax
    mov eax,0x0008
    ;将eax的值赋值给数据段寄存器ds
    mov ds,eax
    ;将段选择子0x0018（栈区）赋值给eax
    mov eax,0x0018
    ;将eax的值赋值给栈段寄存器ss
    mov ss,eax
    ;清空esp寄存器
    xor esp,esp
    ;把内核所在的地址赋值给edi
    mov edi,core_base_address
    ;把内核所在的起始扇区赋值给eax
    mov eax,core_start_sector
    ;把内核所在的地址赋值给ebx
    mov ebx,edi
    ;调用读取硬盘的函数
    call read_hard_disk_0
    ;此时edi的值是内核程序所在的地址，而内核程序的头字节就写了内核程序的大小
    mov eax,[edi]
    ;清空edx寄存器
    xor edx,edx
    ;把512赋值给ecx
    mov ecx,512
    ;eax:edx的值除以ecx的值
    div ecx
    ;如果edx的值不为0，说明内核程序的大小不是512的整数倍
    or edx,edx
    ;edx的值不为0，跳转到@1
    jnz @1
    ;因为已经读取了一个扇区，所以eax的值减1
    dec eax
@1:
    ;如果eax的值为0，说明内核程序的大小就一个扇区的大小
    or eax,eax
    ;eax的值为0，跳转到setup
    jz setup
    ;如果eax的值不为0，说明内核程序的大小大于一个扇区的大小
    ;把eax的值赋值给ecx
    mov ecx,eax
    ;把内核所在的起始扇区赋值给eax
    mov eax,core_start_sector
    ;增加eax的值，指向内核程序的第二个扇区
    inc eax
@2:
    ;调用读取硬盘的函数
    call read_hard_disk_0
    ;增加eax的值，指向内核程序的下一个扇区
    inc eax
    ;循环读取ecx个扇区
    loop @2

setup:
    ;把gdt的地址赋值给esi
    mov esi,[0x7c00 + pgdt + 0x02]
    ;edi寄存器此时的值是内核程序所在的地址
    ;edi+0x04的地址指向内核程序的系统公用例程段位置
    mov eax,[edi + 0x04]
    ;edi+0x08的地址指向内核程序的核心数据段位置
    mov ebx,[edi + 0x08]
    ;ebx的值减去eax的值，得到系统公用历程段大小
    sub ebx,eax
    ;系统公用历程段大小减1，得到实际系统公用历程段大小
    dec ebx
    ;eax的值加上内核程序所在的地址，得到系统公用历程段的基地址
    add eax,edi
    ;ecx的值是段描述符的属性
    mov ecx,0x00409800
    ;调用make_gdt_descriptor函数，制作系统公用历程段的段描述符
    call make_gdt_descriptor
    ;装载内核程序的系统公用历程段的段描述符
    mov [esi + 0x28],eax
    mov [esi + 0x2c],eddx
    ;计算内核程序，整个0-4GB内存的段的大小
    mov eax,[edi + 0x08]
    mov ebx,[edi + 0x0c]
    sub ebx,eax
    dec ebx
    ;计算完毕
    add eax,edi
    ;给ecx寄存器赋值段描述符的属性
    mov ecx,0x00409200
    ;制作内核程序0-4GB内存的段描述符
    call make_gdt_descriptor
    ;装载内核程序的0-4GB内存的段描述符
    mov [esi + 0x30],eax
    mov [esi + 0x34],edx
    ;计算核心代码段的段描述符
    mov eax,[edi + 0x0c]
    mov ebx,[edi + 0x00]
    sub ebx,eax
    dec ebx
    ;计算完毕
    add eax,edi
    mov ecx,0x00409800
    ;制作核心代码段的段描述符
    call make_gdt_descriptor
    ;装载核心代码段的段描述符
    mov [esi + 0x38],eax
    mov [esi + 0x3c],edx
    ;段描述符表的界限
    mov word [0x7c00+pgdt],63
    ;加载GDTR
    lgdt [0x7c00+pgdt]
    ;跳转到内核程序执行
    jmp far [edi + 0x10]

;读取硬盘的函数
read_hard_disk_0:
    ;保存eax的值
    push eax
    ;保存ecx的值
    push ecx
    ;保存edx的值
    push edx
    ;保存eax的值
    push eax

    ;向0x1f2端口写入1，代表要读取一个扇区
    mov dx,0x1f2
    mov al,1
    out dx,al
    ;增加dx的值，指向0x1f3端口
    inc dx
    ;弹出eax的值，此时eax的值是内核程序所在的起始扇区
    pop eax
    ;向0x1f3端口写入起始扇区的低8位
    out dx,al
    ;增加dx的值，指向0x1f4端口
    inc dx
    ;cl的值是8，代表要右移8位
    mov cl,8
    ;把eax右移8位
    shr eax,cl
    ;向硬盘写入起始扇区的8~15位
    out dx,al
    ;增加dx的值，指向0x1f5端口
    inc dx
    ;把eax右移8位
    shr eax,cl
    ;向硬盘写入起始扇区的16~23位
    out dx,al
    ;自增dx寄存器的值，0x1f6端口
    inc dx
    ;把eax右移8位
    shr eax,cl
    ;把eax的高4位设置为1110，代表要用逻辑扇区从硬盘操作
    or al,0xe0
    ;向0x1f6端口写入al的值
    out dx,al
    ;增加dx的值，指向0x1f7端口
    inc dx
    ;向0x1f7端口写入0x20，代表要读取一个扇区
    mov al,0x20
    ;向0x1f7端口写入al的值
    out dx,al

;检查硬盘是否准备好的函数
.waits:
    in al,dx
    and al,0x88
    cmp al,0x08
    jnz .waits
    ;如果硬盘准备好，就从0x1f0端口读取数据
    ;一个字两个字节，所以要读取256次，正好读一个扇区
    mov ecx,256
    mov dx,0x1f0

;从硬盘读取数据的函数
.readw:
    ;从0x1f0端口读取数据，放入ax寄存器
    in ax,dx
    ;把内核程序，读取的两个字节，写入到ebx指向的内存中（规划的内核程序所在地址）
    mov [ebx],ax
    ;ebx的值，是内核程序所在的地址，每次读取两个字节，就把地址增加2
    add ebx,2
    ;循环读取256次
    loop .readw

    ;弹出edx寄存器的值
    pop edx
    ;弹出ecx寄存器的值
    pop ecx
    ;弹出eax寄存器的值
    pop eax
    ret

make_gdt_descriptor:
    ;把要制作的段描述符的基地址赋值给edx
    mov edx,eax
    ;把eax的值左移16位
    shl eax,16
    ;ebx装载的是实际的段大小，此时得到低32位的段描述符
    or ax,bx
    ;把段描述符的后16位清零，只保留高16位
    and edx,0xffff0000
    ;把edx的值循环左移8位，此时16~23位，24~31位都是段基址
    rol edx,8
    ;edx交换高16位和低16位
    bswap edx
    ;把edx寄存器的低16位清零
    xor bx,bx
    ;装配段界限的高4位
    or edx,ebx
    ;装配属性
    or edx,ecx

    ret

pgdt                dw 0
                    dd 0x00007e00

times 510-($-$$)    db 0
                    db 0x55,0xaa